package com.magenta.maxoptra.geocoding.geobase;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.geocoding.geobase.webservice.*;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.utils.DateTimeHelper;
import com.magenta.maxoptra.gis.utils.LineUnits;
import com.magenta.maxoptra.gis.utils.UnitsConverter;
import com.magenta.mrs.util.GeoInformationClient;

import javax.xml.ws.BindingProvider;
import javax.xml.ws.soap.SOAPFaultException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Project: Santa-cruz
 * Author:  Irina Isaeva
 * Created: 07.06.12 10:54
 * <p/>
 * Copyright (c) 1999-2012 Magenta Corporation Ltd. All Rights Reserved.
 * Magenta Technology proprietary and confidential.
 * Use is subject to license terms.
 * <p/>
 * $Id$
 */
public class GeoProviderEchoGeoBaseImpl extends AbstractProviderGeoBase implements IGeocoding, IGeoRouting, IReverseGeocoding  {

    private final String PROPERTY_CLIENT_TIMEOUT = "org.jboss.ws.timeout";
    private static final long MSECS_IN_SECOND = 1000;

    @Override
    public String getProviderName() {
        return "geobase-echo";
    }


    public List<GeoAddress> findAddressByKeyword(GeoProviderSettings settings, String keyword, int i) throws GeoException {
        List<GeoAddress> result = new ArrayList<GeoAddress>();

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
//            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfSurrogateGeocodeAddress geoBaseAddresses = proxy.forwardGeocodeNatural(null, keyword, getCountry(settings));
            if (geoBaseAddresses != null) {
                List<SurrogateGeocodeAddress> surrogateGeocodeAddress = geoBaseAddresses.getSurrogateGeocodeAddress();
                if (surrogateGeocodeAddress != null && !surrogateGeocodeAddress.isEmpty()) {
                    for (SurrogateGeocodeAddress geoBaseAddress : surrogateGeocodeAddress) {
                        GeoAddress resultAddress = createGeoBaseAddress(geoBaseAddress);
                        result.add(resultAddress);
                    }
                }
            }
        } catch (SOAPFaultException e) {
            logger.error(e.getMessage());
        } finally {
            instance.finish(proxy);
        }
        return result;
    }

    public GeoAddress findBestAddressByKeyword(GeoProviderSettings settings, String keyword) throws GeoException {
        GeoAddress address = null;

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfSurrogateGeocodeAddress geoBaseAddresses = proxy.forwardGeocodeNatural(null, keyword, getCountry(settings));

            if (geoBaseAddresses != null) {
                List<SurrogateGeocodeAddress> surrogateGeocodeAddress = geoBaseAddresses.getSurrogateGeocodeAddress();
                if (surrogateGeocodeAddress != null && !surrogateGeocodeAddress.isEmpty()) {
                    SurrogateGeocodeAddress geoBaseAddress = surrogateGeocodeAddress.get(0);
                    address = createGeoBaseAddress(geoBaseAddress);
                }
            }
        } finally {
            instance.finish(proxy);
        }
        return address;
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit) throws GeoException {
        if (address == null) {
            throw new IllegalArgumentException("Address is not specified");
        }
        Country country = getCountry(settings);
        List<GeoAddress> results = new ArrayList<GeoAddress>();

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            // do Forward geocoding
            ArrayOfSurrogateGeocodeAddress gbAddresses = proxy.forwardGeocode("",
                    address.getHouse(),
                    address.getStreet(),
                    address.getCity(),
                    address.getState(),
                    address.getZip(),
                    country);
            if (gbAddresses != null) {
                for (SurrogateGeocodeAddress gbAddress : gbAddresses.getSurrogateGeocodeAddress()) {
                    GeoAddress result = new GeoAddress();
                    result.setCity(gbAddress.getCity());
                    result.setZip(gbAddress.getPostalCode());
                    result.setCountry(gbAddress.getStreet());
                    result.setLatitude(gbAddress.getLatitude());
                    result.setLongitude(gbAddress.getLongitude());
                    results.add(result);
                }
            }
        } finally {
            instance.finish(proxy);
        }
        return results;
    }

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType) throws GeoException {

        if (vehicleType == null) {
            vehicleType = getDefaultVehicleType();
        }

        WsLatLon gbFrom = new WsLatLon();
        double fLat = from.getLatitude();
        double fLon = from.getLongitude();
        gbFrom.setLat(fLat);
        gbFrom.setLon(fLon);

        WsLatLon gbTo = new WsLatLon();
        double tLat = to.getLatitude();
        double tLon = to.getLongitude();
        gbTo.setLat(tLat);
        gbTo.setLon(tLon);

        GeoRoute geoRoute = null;

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            RouteInfo routeInfo = proxy.routeDistanceAndDurationByCoordinates(gbFrom, gbTo, getRouteSettings(vehicleType));
            double distance = routeInfo.getDistance();
            double duration = routeInfo.getDuration();

            distance *= 1.609; // convert to km
            duration *= DateTimeHelper.MSECS_IN_MINUTE;
            long lngDuration = new Double(duration).longValue();

            if (isLoggingAllowed()) {
                logger.info("Route information: from[%s, %s], to[%s, %s], distance=%s, duration=%s",
                        fLat, fLon, tLat, tLon, distance, duration);
            }
            geoRoute = new GeoRoute(distance, lngDuration, from, to);

        } finally {
            instance.finish(proxy);
        }
        return geoRoute;
    }

    @Override
    public synchronized GeoRoute getRoute(List<GeographicCoordinates> coordinates, VehicleType vehicleType) throws GeoException {
        ArrayOfRouteStartStopPoints routesBatch = new ArrayOfRouteStartStopPoints();
        List<RouteStartStopPoints> points = routesBatch.getRouteStartStopPoints();
        if (vehicleType == null) {
            vehicleType = getDefaultVehicleType();
        }

        if (coordinates.size() > 1) {
            for (int i = 0; i < coordinates.size() - 1; i++) {
                GeographicCoordinates from = coordinates.get(i);
                GeographicCoordinates to = coordinates.get(i + 1);
                WsLatLon gbFrom = new WsLatLon();
                gbFrom.setLat(from.getLatitude());
                gbFrom.setLon(from.getLongitude());

                WsLatLon gbTo = new WsLatLon();
                gbTo.setLat(to.getLatitude());
                gbTo.setLon(to.getLongitude());

                RouteStartStopPoints point = new RouteStartStopPoints();
                point.setStart(gbFrom);
                point.setStop(gbTo);
                points.add(point);
            }
        }

        GeoRoute geoRoute = null;

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfRouteEstimation arrayOfRouteEstimation = proxy.routeDistanceAndDurationByCoordinatesBatch(routesBatch, getRouteSettings(vehicleType));
            if (arrayOfRouteEstimation != null && arrayOfRouteEstimation.getRouteEstimation() != null) {
                double distance = 0;
                double duration = 0;
                for (RouteEstimation estimation : arrayOfRouteEstimation.getRouteEstimation()) {
                    RouteInfo routeInfo = estimation.getRouteInfo();
                    if (routeInfo != null) {
                        distance += routeInfo.getDistance();
                        duration += routeInfo.getDuration() * DateTimeHelper.MSECS_IN_MINUTE;
                    }
                }
                long lngDuration = new Double(duration).longValue();
                geoRoute = new GeoRoute(distance, lngDuration, coordinates.get(0), coordinates.get(coordinates.size() - 1));
            }
        } finally {
            instance.finish(proxy);
        }
        return geoRoute;

    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        if (to.isEmpty()) {
            return Collections.emptyList();
        }

        if (vehicleType == null) {
            vehicleType = getDefaultVehicleType();
        }

        ExtendedGeoBaseWs instance = WebServicePool.getInstance().getService(getURL());
        List<GeoRoute> result = new ArrayList<GeoRoute>();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            if (timeout > 0) {
                ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, timeout);
            }

            final WsLatLon origin = FACTORY.createWsLatLon();
            origin.setLat(from.getLatitude());
            origin.setLon(from.getLongitude());

            final ArrayOfWsLatLon destinations = FACTORY.createArrayOfWsLatLon();
            final List<WsLatLon> destinationsList = destinations.getWsLatLon();
            for (final GeographicCoordinates destination : to) {
                final WsLatLon coordinate = FACTORY.createWsLatLon();
                coordinate.setLat(destination.getLatitude());
                coordinate.setLon(destination.getLongitude());
                destinationsList.add(coordinate);
            }

            // IMPORTANT! now use of multiple threads can lead to erroneous results (zero in response)
            final ArrayOfRouteInfo resultFromWs = proxy.routeDistanceAndDurationViaRouteMatrix(origin, destinations, 1, false, getRouteSettings(vehicleType));
            if (resultFromWs != null && !resultFromWs.getRouteInfo().isEmpty()) {
                int i = 0;
                for (final RouteInfo routeInfo : resultFromWs.getRouteInfo()) {
                    final double distance = UnitsConverter.convert(LineUnits.MILE, LineUnits.KILOMETRE, routeInfo.getDistance()); // convert to km
                    final double duration = routeInfo.getDuration()* DateTimeHelper.MSECS_IN_MINUTE;
                    result.add(new GeoRoute(distance, new Double(duration).longValue(), from, to.get(i++)));
                }
            }
        } catch (SOAPFaultException e) {
            logger.error(e.getMessage());
            throw new GeoException(e);
        } finally {
            instance.finish(proxy);
        }

        return result;
    }

    @Override
    public synchronized List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings,
                                                               List<GeographicCoordinates> coordinates) throws GeoException {
        List<GeoLocationRecord> geoLocationRecords = new ArrayList<GeoLocationRecord>();
        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfWsLatLon points = new ArrayOfWsLatLon();
            List<WsLatLon> wsLatLon = points.getWsLatLon();
            for (GeographicCoordinates coordinate : coordinates) {
                WsLatLon location = new WsLatLon();
                location.setLat(coordinate.getLatitude());
                location.setLon(coordinate.getLongitude());
                wsLatLon.add(location);
            }

            ArrayOfWsLatLon resultRoutePoints = proxy.getRoutePoints("", points, getRouteSettings());
            if (resultRoutePoints != null && resultRoutePoints.getWsLatLon().size() > 0) {
                for (WsLatLon resultWsLatLon : resultRoutePoints.getWsLatLon()) {
                    geoLocationRecords.add(new GeoLocationRecord(resultWsLatLon.getLon(), resultWsLatLon.getLat()));
                }
            }
        } finally {
            instance.finish(proxy);
        }
        return geoLocationRecords;
    }

    @Override
    public synchronized List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings,
                                                               GeographicCoordinates from,
                                                               GeographicCoordinates to) throws GeoException {
        List<GeographicCoordinates> coordinates = new ArrayList<GeographicCoordinates>();
        coordinates.add(from);
        coordinates.add(to);
        return getRoutePoints(settings, coordinates);
    }

    public void setClient(GeoInformationClient client) {
        this.client = client;
    }

    protected GisParameters getProviderParams(String regionName, boolean forRouting) {
        return client.getProperties();
    }

    @Override
    public synchronized GeoAddress doReverseGeocoding(GeographicCoordinates geoCoordinates) throws GeoException {
        GeoAddress result = null;
        ArrayOfWsLatLon points = new ArrayOfWsLatLon();
        List<WsLatLon> wsLatLon = points.getWsLatLon();
        WsLatLon location = new WsLatLon();
        location.setLat(geoCoordinates.getLatitude());
        location.setLon(geoCoordinates.getLongitude());
        wsLatLon.add(location);

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfAddressWithStreetLink addressWithStreetLink = proxy.reverseGeocodeFull(null, points);

            if (addressWithStreetLink != null &&
                    addressWithStreetLink.getAddressWithStreetLink() != null &&
                    addressWithStreetLink.getAddressWithStreetLink().size() > 0) {
                AddressWithStreetLink link = addressWithStreetLink.getAddressWithStreetLink().get(0);
                if (link.getAddress() != null) {
                    result = buildGeoAddress(link, geoCoordinates);
                }

            }
        } finally {
            instance.finish(proxy);
        }
        return result;
    }

    @Override
    public List<GeoAddress> doReverseGeocodeFull(List<GeographicCoordinates> geoCoordinates) throws GeoException {
        List<GeoAddress> addresses = new ArrayList<GeoAddress>();

        EchoGeoBaseWs instance = EchoGeoBaseWs.getInstance();
        GeoBaseWsSoap proxy = null;
        try {
            proxy = instance.getGeoBaseWsSoap();
            ((BindingProvider) proxy).getRequestContext().put(PROPERTY_CLIENT_TIMEOUT, 30 * MSECS_IN_SECOND);

            ArrayOfWsLatLon points = new ArrayOfWsLatLon();
            List<WsLatLon> wsLatLon = points.getWsLatLon();
            for (GeographicCoordinates coordinates : geoCoordinates) {
                WsLatLon location = new WsLatLon();
                location.setLat(coordinates.getLatitude());
                location.setLon(coordinates.getLongitude());
                wsLatLon.add(location);
            }

            List<AddressWithStreetLink> addressWithStreetLinks = proxy.reverseGeocodeFull(null, points).getAddressWithStreetLink();

            for (AddressWithStreetLink addressLink : addressWithStreetLinks) {
                if (addressLink.getAddress() != null) {
                    addresses.add(buildGeoAddress(addressLink, null));
                }
            }
        } finally {
            instance.finish(proxy);
        }
        return addresses;
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoutes from coordinate to list of coordinates is not supported for "+getProviderName());
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, null, timeout);
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException {
        return getRoute(from, to, null, timeout);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates) throws GeoException {
        return getRoute(coordinates, null);
    }

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException, ServerUnavailableException {
        return getRoute(from, to, null);
    }

}

